<?php

namespace EthanZ\LaravelExt\Log;

use Carbon\Carbon;
use EthanZ\LaravelExt\Constants\CommonSetting;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Throwable;

/**
 * 日志封装
 */
class Log
{


    /**
     * 递归删除过期日志
     *
     * @param string  $path   目录.
     * @param string  $moth   月期限.
     * @param string  $day    日期限.
     * @param integer $delete 删除状态（1 全删除 2 删除日过期部分 0 不删除）.
     *
     * @return void
     */
    private static function delFolderFiles(string $path, string $moth, string $day, int $delete = 0): void
    {
        $resource = opendir($path);
        while ($file = readdir($resource)) {
            // 排除根目录.
            if ($file === '..' || $file === '.') {
                continue;
            }

            switch (true) {
                case is_dir($path . '/' . $file):
                    // 子文件夹，进行递归.
                    self::delFolderFiles($path . '/' . $file, $moth, $day, $file < $moth ? 1 : 2);
                    break;
                case $delete === 1:
                    // 删除所有文件.
                    unlink($path . '/' . $file);
                    break;
                case $delete === 2:
                    // 删除小于当天的文件.
                    $fileDay = substr($file, 0, 2);
                    if ($fileDay < $day && file_exists($path . '/' . $file)) {
                        unlink($path . '/' . $file);
                    }
                    break;
                default:
                    break;
            }
        }

        if ($delete === 1) {
            rmdir($path);
        }

        closedir($resource);
    }


    /**
     * 首先删除过期日志
     */
    private static function delExpiredLogs(): void
    {
        try {
            $logMaxDay = (new Carbon())->subDays(CommonSetting::LOG_MAX_DAY);
            $moth      = $logMaxDay->format('Ym');
            $day       = $logMaxDay->format('d');
            $path      = storage_path('logs');
            $key       = 'dll:' . Date('Ymd');

            $deleteLogCache = Cache::get($key);
            if (!$deleteLogCache) {
                Cache::put($key, 1, 86400);
                self::delFolderFiles($path, $moth, $day);
            }
        } catch (Throwable $e) {
            report($e);
        }
    }


    /**
     *  写入日志
     *
     * @param string $type    日志类型.
     * @param string $message 写入数据.
     *
     * @return void
     * @throws Throwable
     */
    private static function save(string $type, string $message): void
    {
        if (!$message) {
            return;
        }

        // 首先删除过期日志.
        self::delExpiredLogs();

        // 命令行访问脚本的，加一个cli标识和用户浏览器访问的区分开.
        $fileName = '';
        if (PHP_SAPI === 'cli') {
            $fileName = 'cli_';
        }

        // 拼接文件名.
        $fileName = date('d') . '_' . $fileName . $type . '.log';
        // 拼接路径.
        $path = storage_path('logs/' . date('Ym'));
        // 修改目录权限，防止新建失败.
        self::mkDirs($path);
        // 拼接文件路径.
        $path .= '/' . $fileName;
        // 判断文件是否超过最大限制.
        $path = self::judgeSize($path, $type);
        // 获取url.
        if (isset($_SERVER['HTTP_HOST'])) {
            $currentUri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
        } else {
            $currentUri = 'cmd:' . implode(' ', $_SERVER['argv']);
        }
        // Log内容拼接 - 分隔符.
        $messageFirst = '---------------------------------------------------------------' . "\r\n\r\n";
        // Log内容拼接 - 时间.
        $messageFirst .= '[' . date('Y-m-d H:i:s') . ']';
        // Log内容拼接 - IP、method.
        $ip           = PHP_SAPI === 'cli' ? '' : request()?->getClientIp();
        $method       = (new Request())->method();
        $messageFirst .= ' ' . $ip . ' ' . $method;
        $messageFirst .= ' ' . $currentUri . "\r\n";
        // Log内容拼接 - 信息.
        $message = $messageFirst . $message;
        // 写入.
        file_put_contents($path, $message, FILE_APPEND);
    }


    /**
     *  日志级别
     *
     * @param string $logType 日志类型.
     *
     * @return bool
     */
    public static function logLevel(string $logType): bool
    {
        return Str::contains(env('LOG_LEVEL', 'error,slow'), $logType);
    }


    /**
     *  信息日志
     *
     * @return void
     * @throws Throwable
     */
    public static function info(): void
    {
        // 判断是否开启日志.
        if (!self::logLevel('info')) {
            return;
        }

        // 请求信息.
        $message = self::requestInfo();

        self::save('info', $message);
    }


    /**
     *  请求信息
     *
     * @return string
     */
    public static function requestInfo(): string
    {
        // 计算运行时间.
        $runtime = number_format((microtime(true) - request()?->server('REQUEST_TIME_FLOAT')), 6);
        // 计算吞吐率.
        $reqs    = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞';
        $timeStr = '[运行时间：' . number_format($runtime, 6) . 's][吞吐率：' . $reqs . 'req/s]';
        // 计算文件加载.
        $fileLoad = ' [文件加载：' . count(get_included_files()) . ']' . "\r\n";
        // 获取头部信息.
        $header = request()?->header();
        // 循环返回头部信息为一维数组.
        foreach ($header as $k => $v) {
            $header[$k] = $v[0];
        }
        // 获取参数（过滤掉系统参数）.
        $param = request()?->input();
        unset($param['administrators'], $param['this_user']);

        // Log内容拼接 - IP、method.
        $message = $timeStr . $fileLoad;
        $message .= '[ HEADER ] ' . var_export($header, true) . "\r\n";
        $message .= '[ PARAM ] ' . var_export($param, true) . "\r\n";

        return $message;
    }


    /**
     *  错误日志
     *
     * @param array $errorData 错误信息.
     *
     * @return void
     * @throws Throwable
     */
    public static function error(array $errorData): void
    {
        // 判断是否开启日志.
        if (!self::logLevel('error')) {
            return;
        }

        if ($errorData) {
            // Log内容拼接 - 错误信息.
            $message = '';
            if (isset($errorData['rpc']) && $errorData['rpc']) {
                $message .= '[ RPC ] ' . $errorData['rpc'] . "\r\n";
            }

            $message .= '[ ERROR ] ' . $errorData['msg'] . "\r\n";
            $message .= '[ FILE ] ' . $errorData['file'] . ' on line ' . $errorData['line'] . "\r\n";
            self::save('error', $message);
        }
    }


    /**
     *  Sql日志
     *
     * @param array $query Sql信息.
     *
     * @return void
     * @throws Throwable
     */
    public static function sql(array $query): void
    {
        // 判断是否开启日志.
        if (!self::logLevel('sql')) {
            return;
        }

        // Log内容拼接 - sql信息.
        $message = '';
        foreach ($query as $v) {
            $bindings = [];
            foreach ($v['bindings'] as $va) {
                $bindings[] = is_numeric($va) ? $va : "'$va'";
            }

            $message .= '[ SQL ] excute ' . $v['time'] . 'ms ' . Str::replaceArray('?', $bindings, $v['query']) . "\r\n";
        }

        self::save('sql', $message);
    }


    /**
     *  慢日志
     *
     * @param array $query Sql信息.
     *
     * @return void
     * @throws Throwable
     */
    public static function slow(array $query): void
    {
        // 判断是否开启日志.
        if (!self::logLevel('slow')) {
            return;
        }

        $message = '';
        // 计算运行时间.
        $runtime = number_format((microtime(true) - request()?->server('REQUEST_TIME_FLOAT')), 6);
        if ($runtime > CommonSetting::LOG_RUN_MAX_TIME) {
            $message = self::requestInfo();
        }

        // Log内容拼接 - sql慢日志.
        foreach ($query as $v) {
            $runTime = ($v['time'] / 1000);
            if ($runTime > CommonSetting::LOG_SQL_MAX_TIME) {
                $message .= '[ SQL ] ' . $v['query'] . '  [ RunTime：' . $v['time'] . 'ms]' . "\r\n";
                $message .= '[ BIND ] ' . var_export($v['bindings'], true) . "\r\n";
            }
        }

        self::save('slow', $message);
    }


    /**
     *  给日志文件夹权限
     *
     * @param string  $dir  目录.
     * @param integer $mode 权限.
     *
     * @return boolean
     */
    private static function mkDirs(string $dir, int $mode = 0777): bool
    {
        if (is_dir($dir)) {
            return true;
        }

        if (!self::mkdirs(dirname($dir), $mode)) {
            return false;
        }

        return @mkdir($dir, $mode);
    }


    /**
     *  判断文件是否超过最大限制
     *
     * @param string  $path   目录.
     * @param string  $type   日志类型.
     * @param integer $number 第几次.
     *
     * @return string
     */
    private static function judgeSize(string $path, string $type, int $number = 0): string
    {
        if (!CommonSetting::LOG_FILE_SIZE && is_file($path)
            && floor(CommonSetting::LOG_FILE_SIZE) <= filesize($path)
        ) {
            $number++;
            $path = substr($path, 0, (strpos($path, $type) + strlen($type))) . '_'
                    . $number . substr($path, strpos($path, '.log'));
            $path = self::judgeSize($path, $type, $number);
        }

        return $path;
    }
}

